/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.primitives.dbm;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;

import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.CodeRunner;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.FalseExit;
import fr.x9c.cadmium.kernel.Misc;
import fr.x9c.cadmium.kernel.Value;

/**
 * This interface defines a Dbm implementation based on a <tt>java.util.Map</tt>.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.1
 * @since 1.1
 */
final class BasicImplementation implements DbmImplementation {

    /** Suffix for database files. */
    private static final String FILE_SUFFIX = ".db";

    /** Flag allowing to read data. */
    private static final int READ = 1;

    /** Flag allowing to write data. */
    private static final int WRITE = 2;

    /** Flag allowing to create data file. */
    private static final int CREATE = 4;

    /** Flags for Dbm operations. */
    private static final int[] FLAGS = {
        BasicImplementation.READ,
        BasicImplementation.WRITE,
        BasicImplementation.READ | BasicImplementation.WRITE,
        BasicImplementation.CREATE
    };

    /**
     * Empty constructor.
     */
    BasicImplementation() {
    } // end empty constructor

    /**
     * {@inheritDoc}
     */
    public Value open(final CodeRunner ctxt,
                      final Value file,
                      final Value flags,
                      final Value mode)
        throws Fail.Exception, FalseExit {
        try {
            final File f = ctxt.getContext().getRealFile(file.asBlock().asString()
                                                         + BasicImplementation.FILE_SUFFIX);

            final Value res = createDbm(f, Misc.convertFlagList(flags, BasicImplementation.FLAGS));
            try {
                fr.x9c.cadmium.util.Misc.setPermissions(f, mode.asLong());
            } catch (final Exception e) {
                // fails silently
            } // end try/catch
            return res;
        } catch (final InterruptedIOException iioe) {
            final FalseExit fe =
                FalseExit.createFromContext(ctxt.getContext());
            fe.fillInStackTrace();
            throw fe;
        } catch (final IOException ioe) {
            Cldbm.error(ctxt, "Can't open/create file");
            return Value.UNIT; // never reached
        } catch (final Exception e) {
            Cldbm.error(ctxt, e.toString());
            return Value.UNIT; // never reached
        } // end try/catch
    } // end method 'open(CodeRunner, Value, Value, Value)'

    /**
     * {@inheritDoc}
     */
    public Value close(final CodeRunner ctxt,
                       final Value db)
        throws Fail.Exception, FalseExit {
        try {
            get(ctxt, db).close();
        } catch (final InterruptedIOException iioe) {
            final FalseExit fe =
                FalseExit.createFromContext(ctxt.getContext());
            fe.fillInStackTrace();
            db.asBlock().setCustom(null);
            throw fe;
        } catch (final IOException ioe) {
            // nothing to do ...
        } catch (final Fail.Exception fe) {
            db.asBlock().setCustom(null);
            throw fe;
        } catch (final Exception e) {
            // nothing to do ...
        } // end try/catch
        db.asBlock().setCustom(null);
        return Value.UNIT;
    } // end method 'close(CodeRunner, Value)'

    /**
     * {@inheritDoc}
     */
    public Value fetch(final CodeRunner ctxt,
                       final Value db,
                       final Value key)
        throws Fail.Exception, FalseExit {
        try {
            final byte[] res =
                get(ctxt, db).db.get(new Key(key.asBlock().getBytes()));
            if (res != null) {
                return Value.createFromBlock(Block.createString(res));
            } else {
                Fail.raiseNotFound();
                return Value.UNIT; // never reached
            } // end if/else
        } catch (final Fail.Exception fe) {
            throw fe;
        } catch (final Exception e) {
            Cldbm.error(ctxt, e.toString());
            return Value.UNIT; // never reached
        } // end try/catch
    } // end method 'fetch(CodeRunner, Value, Value)'

    /**
     * {@inheritDoc}
     */
    public Value insert(final CodeRunner ctxt,
                        final Value db,
                        final Value key,
                        final Value content)
        throws Fail.Exception, FalseExit {
        final Map<Key, byte[]> database = get(ctxt, db).db;
        try {
            final Key k = new Key(key.asBlock().getBytes());
            final byte[] res = database.get(k);
            if (res != null) {
                Cldbm.error(ctxt, "Entry already exists");
                return Value.UNIT; // never reached
            } else {
                database.put(k, content.asBlock().getBytes());
                return Value.UNIT;
            } // end if/else
        } catch (final Fail.Exception fe) {
            throw fe;
        } catch (final Exception e) {
            Cldbm.error(ctxt, e.toString());
            return Value.UNIT; // never reached
        } // end try/catch
    } // end method 'insert(CodeRunner, Value, Value, Value)'

    /**
     * {@inheritDoc}
     */
    public Value replace(final CodeRunner ctxt,
                         final Value db,
                         final Value key,
                         final Value content)
        throws Fail.Exception, FalseExit {
        final Map<Key, byte[]> database = get(ctxt, db).db;
        try {
            database.put(new Key(key.asBlock().getBytes()),
                         content.asBlock().getBytes());
            return Value.UNIT;
        } catch (final Exception e) {
            Cldbm.error(ctxt, e.toString());
            return Value.UNIT; // never reached
        } // end try/catch
    } // end method 'replace(CodeRunner, Value, Value, Value)'

    /**
     * {@inheritDoc}
     */
    public Value delete(final CodeRunner ctxt,
                        final Value db,
                        final Value key)
        throws Fail.Exception, FalseExit {
        final Map<Key, byte[]> database = get(ctxt, db).db;
        try {
            database.remove(new Key(key.asBlock().getBytes()));
            return Value.UNIT;
        } catch (final Exception e) {
            Cldbm.error(ctxt, e.toString());
            return Value.UNIT; // never reached
        } // end try/catch
    } // end method 'delete(CodeRunner, Value, Value)'

    /**
     * {@inheritDoc}
     */
    public Value firstkey(final CodeRunner ctxt,
                          final Value db)
        throws Fail.Exception {
        final DB database = get(ctxt, db);
        // the iterator is taken from a copy of the actual set
        // to avoid any concurrent modification exception
        final Iterator<Key> it = new LinkedHashSet<Key>(database.db.keySet()).iterator();
        database.iterator = it;
        if (!it.hasNext()) {
            Fail.raiseNotFound();
        } // end if
        return Value.createFromBlock(Block.createString(it.next().bytes));
    } // end method 'firstkey(CodeRunner, Value)'

    /**
     * {@inheritDoc}
     */
    public Value nextkey(final CodeRunner ctxt,
                         final Value db)
        throws Fail.Exception {
        final Iterator<Key> it = get(ctxt, db).iterator;
        if ((it == null) || !it.hasNext()) {
            Fail.raiseNotFound();
        } // end if
        return Value.createFromBlock(Block.createString(it.next().bytes));
    } // end method 'nextkey(CodeRunner, Value)'

    /**
     * Creates a <i>Dbm.t</i> instance from a file.
     * @param f file to create instance from - should not be <tt>null</tt>
     * @return a <i>Dbm.t</i> instance from a file
     */
    private static Value createDbm(final File f, final int flags) throws IOException {
        assert f != null : "null f";
        final Block res = Block.createBlock(1, Block.ABSTRACT_TAG);
        res.setCustom(new DB(f, flags));
        return Value.createFromBlock(res);
    } // end method 'createDbm(File)'

    /**
     * Returns the {@link fr.x9c.cadmium.primitives.dbm.BasicImplementation.DB} object
     * encapsulated inside a value.
     * @param ctxt context - should not be <tt>null</tt>
     * @param db value to get data object from
     *           - should not be <tt>null</tt>
     * @return the {@link fr.x9c.cadmium.primitives.dbm.BasicImplementation.DB} object
     *         encapsulated inside a value
     * @throws Fail.Exception if <tt>db</tt> has been closed
     */
    private static DB get(final CodeRunner ctxt,
                          final Value db)
        throws Fail.Exception {
        assert ctxt != null : "null ctxt";
        assert db != null : "null db";
        final DB res = (DB) db.asBlock().asCustom();
        if (res != null) {
            return res;
        } else {
            Cldbm.error(ctxt, "DBM has been closed");
            return null; // never reached
        } // end if/else
    } // end method 'get(CodeRunner, Value)'

    /**
     * This inner-class implements a simple wrapper around a byte array.
     * This wrapper is needed to have an object that provides
     * <i>correct</i> implementations for <tt>hashCode()</tt>
     * and <tt>equals(Object)</tt>.
     * Such methods are needed to use a byte array as a key value
     * in a map.
     */
    private static final class Key implements Serializable {

        /** Serialization UID. */
        static final long serialVersionUID = -8464091220789916051L;

        /** Wrapped value. */
        private final byte[] bytes;

        /**
         * Wraps a byte array.
         * @param b byte array to wrap - should not be <tt>null</tt>
         */
        private Key(final byte[] b) {
            assert b != null : "null b";
            this.bytes = Arrays.copyOf(b, b.length);
        } // end constructor(byte[])

        /**
         * {@inheritDoc}
         */
        @Override
        public int hashCode() {
            return Arrays.hashCode(this.bytes);
        } // end method 'hashCode()'

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(final Object obj) {
            if (obj instanceof Key) {
                final Key that = (Key) obj;
                return Arrays.equals(this.bytes, that.bytes);
            } else {
                return false;
            } // end if/else
        } // end method 'equals(Object)'

    } // end inner-class 'Key'

    /**
     * This inner-class is a mere structure describing a database.
     */
    private static final class DB {

        /** Actual database. */
        private final Map<Key, byte[]> db;

        /** Actual file. */
        private final File file;

        /** Allowed database operations. */
        private final int flags;

        /** Current enumeration over keys. */
        private Iterator<Key> iterator;

        /**
         * Constructs a structure from a database. <br/>
         * Enumeration is set to <tt>null</tt>.
         * @param f database file - should not be <tt>null</tt>
         * @param flags flag used for file opening
         */
        @SuppressWarnings("unchecked")
        private DB(final File f, final int flags) throws IOException {
            assert f != null : "null f";
            if (((flags & READ) != 0) && f.exists()) {
                final InputStream is = new FileInputStream(f);
                final ObjectInputStream ois = new ObjectInputStream(is);
                try {
                    this.db = (Map<Key, byte[]>) ois.readObject();
                } catch (final ClassNotFoundException cnfe) {
                    throw new IOException("invalid file format");
                } catch (final ClassCastException cce) {
                    throw new IOException("invalid file format");
                } // end try/catch
                try {
                    ois.close();
                } catch (final Exception e) {
                    // fail silently
                } // end try/catch
                try {
                    is.close();
                } catch (final Exception e) {
                    // fail silently
                } // end try/catch
            } else {
                this.db = new HashMap<Key, byte[]>();
            } // end if/else
            this.file = f;
            this.flags = flags;
            this.iterator = null;
        } // end constructor(File, int)

        /**
         * Closes the database, writing data if requested.
         */
        private void close() throws IOException {
            if ((this.flags & WRITE) != 0) {
                final OutputStream os = new FileOutputStream(this.file);
                final ObjectOutputStream oos = new ObjectOutputStream(os);
                oos.writeObject(this.db);
                try {
                    oos.close();
                } catch (final Exception e) {
                    // fail silently
                } // end try/catch
                try {
                    os.close();
                } catch (final Exception e) {
                    // fail silently
                } // end try/catch
            } // end if
        } // end method 'close()'

        /**
         * {@inheritDoc}
         */
        protected void finalize() {
            try {
                close();
            } catch (final IOException ioe) {
                // fail silently
            } // try/catch
        } // end method 'finalize()'

    } // end inner-class 'DB'

} // end class 'BasicImplementation'
